Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Developing Windows Forms Control using MFC and Managed C++

0.00/5 (No votes)
12 Mar 2002 1  
Demonstrates different ways to move MFC based controls to .NET Windows Forms

Introduction

Windows Forms is the framework for developing rich client GUI applications under the .NET framework. There are many cool features in Windows Forms which greatly simplify development. The problem is that all the cool controls available at codeproject are written in MFC and cannot be used on Windows Forms Application directly. I know of at least three different ways by which existing controls can be migrated to .NET :-

  1. Rewriting the controls completely in managed code
  2. Making them ActiveX controls and using the ActiveX control on windows forms
  3. Using managed C++.

The purpose of this article is to demonstrate the last of the above techniques.

Demo Control

In order to demonstrate these techniques I select one of my favorite controls in codeproject Mark C. Malburg's Analog Meter Control. The aim is to be able to develop a windows forms control by wrapping the existing MFC control (with little or no changes to the original MFC code).  The final Windows Forms control developed can be placed on windows forms designer as shown in the image.

How the control looks like in the designer

The control has following properties :-

Property Name Property Type Description
Units System::String The text of units shown in the meter
Value double The value which determines the needle position and also shown at the bottom of the control
NeedleColor System::Drawing::Color The color of the needle

In addition the control supports a managed event called  OnValueChanged which is fired whenever the Value property is changed.

To start with we need to create a managed C++ class library with MFC support. I wrote a wizard that does it. The wizard is downloadable from http://www.codeproject.com/useritems/ManagedMFCDLL.asp. Download and install the wizard and also download the source for Analog Meter Control.

Using Managed C++ With MFC

Our aim is to retain as much existing MFC code as possible because all this code has been tested and runs well (assuming). VC++.NET allows to mix managed (a code that is compiled to IL) and unmanaged (native) code. It can even compile existing code to IL. But you cannot use the classes so compiled in other .NET languages unless they are marked with <code>__gc prefix. e.g <code>__gc class MyControl . A class that is marked __gc cannot derive from any class not marked __gc. This rules out use of CObject, CWnd classes as base class as thet are not marked __gc. The other restriction is that __gc classes cannot contain members of any (practically) other classes. But a __gc class can contain a pointer to MFC class. 

In order to develop a windows forms control we need to create a class that extends the managed class (marked with __gc) System::Windows::Forms::Control class just like an MFC custom control extends CWnd.  Within this managed object we will contain an instance of the MFC class. We will make the implementation of properties and methods of the managed control delegate to the MFC class and let the MFC code do all the hard work. The problem is that we need to associate the same window handle (all controls are windows) to both the MFC object and the System::Windows::Forms::Control object. This can be done in two ways :-

  1. Let the windows form control create its window an MFC subclass it.
  2. Let the windows form control superclass the window used by the MFC control.

Both the methods are covered in the article.

Subclassing Windows Forms control through MFC

We start with creating a blank VS.NET solution with any name.

Add a Managed MFC DLL project to the solution using the wizard named Control. This project will be used for any managed classes.

Next add to the solution a Win32 static library project with support  for MFC and precompiled headers. Call this ControlS (S stands for static). This will contain the MFC code for the control. Separation of managed and unmanaged code makes maintenance a bit easier. We will  make the project "Control" dependent on "ControlS" to link them together.

Place the files 3DMeterCtrl.cpp, 3DMeterCtrl.h and MemDC.h in the "ControlS" project. Modify the 3DMeterCtrl.cpp file to remove #include "MeterTestForm.h" as shown

#include "stdafx.h"

#include "math.h"

#include "3DMeterCtrl.h"

//#include "MeterTestForm.h" This line is to be removed

#include "MemDC.h"

            

After this the project ControlS would build successfully.

We need to write a managed wrapper for the control. We call this class ThreeDMeter and it should derive from System::Windows::Forms::Control. In order to do this we need to import the required assemblies add required #using's to stdafx.h as shown. Any code that needs to be added is shown in blue

#include <afxwin.h>         // MFC core and standard components

#include "..\Controls\3DMeterCtrl.h"

#using <mscorlib.dll>
#using <system.drawing.dll>
#using <system.dll>
#using <system.design.dll>
#using <system.windows.forms.dll>

Now we can create the main control class. Replace the default class generated by the wizard with class ThreeDMeter.

// Control.h


#pragma once

#include "resource.h"        // main symbols


using namespace System;
using namespace System::Drawing;
using namespace System::Windows::Forms;
using namespace System::Runtime::InteropServices;
using namespace System::Runtime::Remoting::Messaging;

namespace ControlDemo
{
    public __gc class ThreeDMeter : public Control
    {
    public:
        ThreeDMeter()
        {
            m_pCtrl = new C3DMeterCtrl();
        }
    protected:
        void Dispose(bool b)
        {
            Control::Dispose(b);
            
            if (m_pCtrl != NULL)
            {
                delete m_pCtrl;
                m_pCtrl = NULL;
            }
        }

        void OnHandleCreated(EventArgs* e)
        {
            System::Diagnostics::Debug::Assert(m_pCtrl->GetSafeHwnd() == NULL);
            
            m_pCtrl->SubclassWindow((HWND)get_Handle().ToPointer());

            Control::OnHandleCreated(e);
        }
    
    private:
        C3DMeterCtrl* m_pCtrl;
    };
}        
        

Compile and build the DLL. At this time we have got a windows forms control that can be used in a C# or a VB application. To test the control, launch another instance of VS.NET and create a VB or a VC# Windows Application. Add the ThreeDMeter control to the toolbox (Click here if you want to know how to do it). Double clicking on the ThreeDMeter toolbox item adds the control to the form as shown

If you get any errors try modifying the copy local property of the reference to Control assembly to false as shown below (I am not sure why this works, I would be glad if anyone can explain me why setting Copy Local to false makes fixes the problem)

So we have succeeded in wrapping an MFC control around a managed class and using it in the windows forms designer. How exactly does it work?

A windows forms control is a window with style of WS_CHILD by default. When a control is added to the form the window handle is created. When this happens System::Windows::Forms::Control 's protected method OnHandleCreated is called. We overload this method and subclass it with the C3DMeterCtrl object we create, using SubclassWindow method. If you are not familiar with subclassing refer to Chris Maunder's tutorial on Subclassing.

Even though our control paints successfully and can be used in the designer there is not much user can do to modify the behavior of the control. (like changing the needle color or changing the text of units etc.) In the next step we would add properties in the control that would allow us to do this.

Lets add code add the properties using managed extensions to C++ keyword __property. After adding the properties the code looks like this

__property Color get_NeedleColor()
{
    if (!m_pCtrl)
        throw new ObjectDisposedException(__typeof(ThreeDMeter)->ToString());

    return System::Drawing::ColorTranslator::FromWin32(m_pCtrl->m_colorNeedle);
}

__property void set_NeedleColor(Color clr)
{
    if (!m_pCtrl)
        throw new ObjectDisposedException(__typeof(ThreeDMeter)->ToString());

    AFX_MANAGE_STATE(AfxGetStaticModuleState());
            
    m_pCtrl->SetNeedleColor(ColorTranslator::ToWin32(clr));
}

__property void set_Units(String* units)
{
    if (!m_pCtrl)
        throw new ObjectDisposedException(__typeof(ThreeDMeter)->ToString());

    AFX_MANAGE_STATE(AfxGetStaticModuleState());

    CString strUnits(units);
            
    m_pCtrl->SetUnits(strUnits);
}
        
__property String* get_Units()
{
    if (!m_pCtrl)
        throw new ObjectDisposedException(__typeof(ThreeDMeter)->ToString());

    LPCTSTR szUnits = (m_pCtrl->m_strUnits);

    return new String(szUnits);
}
        
__property double get_Value()
{
    if (!m_pCtrl)
        throw new ObjectDisposedException(__typeof(ThreeDMeter)->ToString());
            
    return m_pCtrl->m_dCurrentValue;
}
        
__property void set_Value(double d)
{
    if (!m_pCtrl)
        throw new ObjectDisposedException(__typeof(ThreeDMeter)->ToString());
            
    AFX_MANAGE_STATE(AfxGetStaticModuleState());
            
    m_pCtrl->UpdateNeedle(d);

    OnValueChanged(this, EventArgs::Empty);
}
        

These properties can be called at any time even when the handle is not created. So we need to make sure that none of the MFC methods that make use of Windows handle get called before the window is created this would lead MFC to fire assertions. We fix this in the MFC code in 3DMeterCtrl.cpp as shown

void C3DMeterCtrl::ReconstructControl() 
{
    if (!GetSafeHwnd())
        return;

    // if we've got a stored background - remove it!

    if ((m_pBitmapOldBackground) && 
          (m_bitmapBackground.GetSafeHandle()) && 
            (m_dcBackground.GetSafeHdc()))
    {
            m_dcBackground.SelectObject(m_pBitmapOldBackground);
            m_dcBackground.DeleteDC() ;
            m_bitmapBackground.DeleteObject();
    }
    
    Invalidate () ;
}

The above code demostrates a few points

  1. Using properties in C++ (refer to Chris Maunders tutorial on managed C++ properties for more details).
  2. Changing System::Drawing::Color to COLORREF using ColorTranslator
  3. Changing System::String type to CString using the new CString constructor for System::String. (Thanks to Anson Tsao for pointing that to me)
  4. Use of AFX_MANAGE_STATE to ensure proper MFC state. Those of you who developed COM objects using MFC and ATL must be familiar with this.

What we have actually done is to delegate the calls C3DMeterCtrl class. After you build the project it would be possible to set or change these properties from the designer and instantly see the change in the rendering of the control on the VB/VC# form.

It would be really cool if the properties can be organized in the property grid. This can be done simply by using managed C++ attributes e.g.

[property: System::ComponentModel::CategoryAttribute("Meter")]
__property Color get_NeedleColor()
{
    if (!m_pCtrl)
        throw new ObjectDisposedException(__typeof(ThreeDMeter)->ToString());

    return System::Drawing::ColorTranslator::FromWin32(m_pCtrl->m_colorNeedle);
}
        

In the above example we have applied CategoryAttribute to the property NeedleColor. The effect of this attribute can be seen in the property grid after you build the project with these changes.

The thing which is missing from the Control is that it doesn't fire any events. An example event could be OnValueChanged fired when the value changes. The following declaration indicates that the control supports OnValueChanged event.

__event EventHandler * OnValueChanged; 
        

In order to fire the event the set_Value method can be changed as following

__property void set_Value(double d)
{
    if (!m_pCtrl)
        throw new ObjectDisposedException(__typeof(ThreeDMeter)->ToString());
            
    AFX_MANAGE_STATE(AfxGetStaticModuleState());
        
    m_pCtrl->UpdateNeedle(d);

    OnValueChanged(this, EventArgs::Empty);
}
        

Thus we have a completely functional control based on an MFC control. The problem with this implementation is that MFC control gets a first shot at the events and not the managed control. This would be a major problem if the control needs to be enhanced later on in managed code for example by deriving another control written C# from this control. This brings us to the next technique - allowing windows forms to superclass our MFC control so that windows forms control gets a first shot at the messages.

Allowing Windows Forms control to superclass existing MFC controls

The System::Windows::Forms::Control class can use an existing Window Class to create its window. This can be done by overloading get_CreateParams method and specifying a new class name. The only restriction is that Window class should be registered with CS_GLOBALCLASS. So we register a new window class in InitInstance and unregister it in ExitInstance as shown

BOOL CControlApp::InitInstance()
{
    CWinApp::InitInstance();
    
    WNDCLASS wc;

    memset(&wc, 0, sizeof(wc));

    wc.lpszClassName = "Analog3dMeter";
    wc.hInstance = m_hInstance;
    wc.lpfnWndProc = Analog3dMeterWindowProc;
    wc.style = CS_DBLCLKS | CS_GLOBALCLASS | CS_HREDRAW | CS_VREDRAW;

    return RegisterClass(&wc);
}

int CControlApp::ExitInstance()
{
    UnregisterClass("Analog3dMeter", m_hInstance);

    return CWinApp::ExitInstance();
}        
        

Now we can overload get_CreateParams method. For demonstration purposes we create a totally new class called ThreeDMeter2 with much of the code same as ThreeDMeter except for the protected methods.

protected:
    void Dispose(bool b)
    {
        Control::Dispose(b);
        
        m_pCtrl = NULL;
    }

    __property System::Windows::Forms::CreateParams * get_CreateParams()
    {
        System::Windows::Forms::CreateParams * pParams = 
                                          Control::get_CreateParams();

        pParams->ClassName = S"Analog3dMeter";
            
        return pParams;
    }
        

Specifying class name as Analog3dMeter makes the window forms control use this window class to create the window for the control. Now the problem is how do we associate m_pCtrl with the window handle so created. This is done at two places. First the CreateHandle method of a control which is actually responsible for creating the window.

void CreateHandle()
{
    __try
    {
        CallContext::SetData(S"Controls.CurrentControl", 
                             __box(IntPtr(m_pCtrl)));
        
        Control::CreateHandle();
    }
    __finally
    {
        CallContext::SetData(S"Controls.CurrentControl", NULL);
    }
}    
        

CallContext is an equivalent (kind of) of ThreadLocal storage here. In the above code we have set a named property for the thread called "Controls.CurrentControl" to the pointer value of  m_pCtrl. Observe __box operator which is used to convert a value type to Object* which is the type of SetData 's second parameter. After setting this property we call base classes implementation which actually calls CreateWindowEx and finally we clear the thread property.

Now the question is how to associate the window handle with the object. The place we need to do is in the Analog3dMeterWindowProc which is the window procedure. The code of the window procedure looks like this

//Retrieves the pointer set earlier in CreateHandle

CWnd* GetPointerFromCallContext()
{
    IntPtr ip = *dynamic_cast(CallContext::GetData(S"Controls.CurrentControl"));
    
    return (CWnd*)ip.ToPointer();
}

#pragma unmanaged

LRESULT CALLBACK Analog3dMeterWindowProc(HWND hwnd, UINT msg, 
                                         WPARAM wp, LPARAM lp)
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState());

    CWnd* pWnd = CWnd::FromHandlePermanent(hwnd);
    
    if (pWnd == NULL)
    {
        pWnd = GetPointerFromCallContext();
        ASSERT(pWnd != NULL);
        pWnd->Attach(hwnd);
    }
    
    LRESULT ret = AfxCallWndProc(pWnd, hwnd, msg, wp, lp);
    
    if (msg == WM_NCDESTROY)
        delete pWnd;

    return ret;
}

In the window procedure we first check to see if the window is already in the permanent handle map. If not (which is the case when the control gets the WM_NCCREATE message), obtain the object pointer from call context, which we set originally in the CreateHandle method, and attach the window handle to it. Once we have done that we call AfxCallWndProc to do all the hard work of window message handling. Finally, we delete the object pointer when we get WM_NCCDESTROY message.

Looking at the window through spy++ shows certain interesting things :-

Observe that the name of the class in WindowForms10.Analog3dMeter.xxx This is because windows forms framework superclasses our window class and uses the superclass to create our control.

There is one small implementation detail which I in the constructor of the ThreeDMeter2

ThreeDMeter2()
{
    m_pCtrl = new C3DMeterCtrl();
    SetStyle(ControlStyles::UserPaint, false);
}

The SetStyle(ControlStyles::UserPaint, false) makes it possible for the WM_PAINT messages to be forwarded to the original window proc Analog3dMeterWindowProc instead of them being handled in the managed code.

The class ThreeDMeter2 demonstrates that way windows forms superclasses an existing window class. We could have managed to do the something without creating a superclassed window. The control ThreeDMeter3 demonstrates this.

void DefWndProc(Message* m)
{
    if (m_pCtrl)
    {
        if (!m_pCtrl->GetSafeHwnd())
        {
            m_pCtrl->Attach((HWND)m->HWnd.ToPointer());
        }

        m->Result = AfxCallWndProc(m_pCtrl, (HWND)m->HWnd.ToPointer(), 
                                   m->Msg, (WPARAM)m->WParam.ToPointer(), 
                                   (LPARAM)m->LParam.ToPointer());
    }
    else
        //This woul happen after destroy messages

        Control::DefWndProc(m);
}

void OnHandleDestroyed(EventArgs* e)
{
    if (m_pCtrl)
    {
        m_pCtrl->Detach();
        delete m_pCtrl;
        m_pCtrl = NULL;
    }
}

In ThreeDMeter3 we don't create any separate window class. We don't overload CreateHandle or get_CreateParams. We let Control do it's default creation. Instead we load DefWndProc and OnHandleDestroyed. DefWndProc is the function responsible for forwarding the call to the superclassed window procedure (if the control superclassed any window or else to the DefWindowProc function). We overload that and forward the calls to m_pCtrl using AfxCallWndProc. Finally we detach and delete m_pCtrl in OnHandleDestroyed function. Even though this method looks simpler this is not very clean (in my opinion) as there are two potential routes to DefWindowProc one through MFC implementation and other through System::Windows::Forms::Control 's implementation.

Thus, in short I have covered certain ways by which existing MFC controls can be moved to Windows Forms without completely rewriting them.

My special thanks to Mark C. Malburg for making his code available. My special thanks to Essam Ahmed for proof reading the article.

Updates

3/13/2002

  1. Modified string properties to use CString constructor for System::String*
  2. Added ThreeDMeter3

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here